#pragma once

#include <Wincodec.h>
#pragma comment(lib, "Windowscodecs.lib")
#include <d2d1.h>
#pragma comment(lib, "d2d1")

template <typename T>
inline void SafeRelease(T *&p) { if(p) { p->Release(); p = 0; } }

enum DISPOSAL_METHODS { DM_UNDEFINED = 0, DM_NONE = 1, DM_BACKGROUND = 2, DM_PREVIOUS = 3 };
const FLOAT DEFAULT_DPI = 96.f; // Default DPI that maps image resolution directly to screen resoltuion


//
class Gif: public Window
{
   HRESULT hr;

public:

   ID2D1Factory *m_pD2DFactory; ID2D1HwndRenderTarget *m_pHwndRT;
   ID2D1BitmapRenderTarget *m_pD2DBitmapRT; ID2D1Bitmap *m_pD2DBitmap, *m_pSavedFrame;
   D2D1_COLOR_F m_backgroundColor; IWICImagingFactory *m_pIWICFactory; IWICBitmapDecoder *m_pBitmapDecoder;

   UINT    m_uNextFrameIndex;
   UINT    m_uTotalLoopCount;  // The number of loops for which the animation will be played
   UINT    m_uLoopNumber;      // The current animation loop number (e.g. 1 when the animation is first played)
   BOOL    m_fHasLoop;         // Whether the gif has a loop
   UINT    m_cFrames, m_uFrameDisposal, m_uFrameDelay, m_cxGifImage, m_cyGifImage;
   UINT    m_cxGifImagePixel;  // Width of the displayed image in pixel calculated using pixel aspect ratio
   UINT    m_cyGifImagePixel;  // Height of the displayed image in pixel calculated using pixel aspect ratio
   D2D1_RECT_F m_framePosition;

   //
   Gif() 
   {
      m_pD2DFactory=0; m_pHwndRT=0; m_pD2DBitmapRT=0; m_pD2DBitmap=0; m_pSavedFrame=0; //bitmap used for disposal 3 method
      m_backgroundColor = D2D1::ColorF(0, 0.f);  m_pIWICFactory=0;  m_pBitmapDecoder=0;
   }

   //
   ~Gif()
   {
      SafeRelease(m_pD2DFactory); SafeRelease(m_pHwndRT); SafeRelease(m_pD2DBitmapRT);
      SafeRelease(m_pD2DBitmap); SafeRelease(m_pSavedFrame); SafeRelease(m_pIWICFactory); SafeRelease(m_pBitmapDecoder);
   }

   //
   int OnCreate(WPARAM, LPARAM)
   {
      D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &m_pD2DFactory);
      CoCreateInstance(CLSID_WICImagingFactory, NULL, CLSCTX_INPROC_SERVER, IID_PPV_ARGS(&m_pIWICFactory));
      DisplayGif();
      return 0;
   }

   //
   void DisplayGif()
   {
      m_uNextFrameIndex = 0; m_uFrameDisposal = DM_NONE; //No previous frame, use disposal none
      m_uLoopNumber = 0; m_fHasLoop = FALSE; SafeRelease(m_pSavedFrame); SafeRelease(m_pBitmapDecoder);
      hr = m_pIWICFactory->CreateDecoderFromFilename( L"test.gif", 0, GENERIC_READ, WICDecodeMetadataCacheOnLoad, &m_pBitmapDecoder);
      hr = GetGlobalMetadata(); SetWindowPos(hwnd, 0, 0, 0, m_cxGifImagePixel, m_cyGifImagePixel, SWP_NOMOVE); 
      hr = CreateDeviceResources();
      //If we have at least one frame, start playing the animation from the first frame
      if(m_cFrames > 0) { hr = ComposeNextFrame(); InvalidateRect(hwnd, NULL, FALSE); }
   }


   //
   HRESULT GetGlobalMetadata()
   {
      PROPVARIANT propValue; PropVariantInit(&propValue); IWICMetadataQueryReader *pMetadataQueryReader = NULL;
      m_pBitmapDecoder->GetFrameCount(&m_cFrames); m_pBitmapDecoder->GetMetadataQueryReader( &pMetadataQueryReader );
      if(FAILED( GetBackgroundColor(pMetadataQueryReader) )) { m_backgroundColor = D2D1::ColorF(0, 0.f); } //transparent

      if(SUCCEEDED(hr))
      {
         hr = pMetadataQueryReader->GetMetadataByName(L"/logscrdesc/Width", &propValue);
         hr = (propValue.vt == VT_UI2 ? S_OK : E_FAIL); if(SUCCEEDED(hr)) m_cxGifImage = propValue.uiVal; PropVariantClear(&propValue);
      }
      if (SUCCEEDED(hr))
      {
         hr = pMetadataQueryReader->GetMetadataByName(L"/logscrdesc/Height", &propValue);
         if (SUCCEEDED(hr))
         { 
            hr = (propValue.vt == VT_UI2 ? S_OK : E_FAIL);
            if(SUCCEEDED(hr)) m_cyGifImage = propValue.uiVal; PropVariantClear(&propValue);
         }
      }

      if(SUCCEEDED(hr))
      {
         hr = pMetadataQueryReader->GetMetadataByName(L"/logscrdesc/PixelAspectRatio", &propValue);
         if(SUCCEEDED(hr))
         {
            hr = (propValue.vt == VT_UI1 ? S_OK : E_FAIL);
            if(SUCCEEDED(hr))
            {
               UINT uPixelAspRatio = propValue.bVal;
               if(uPixelAspRatio)
               {
                  //Need to calculate the ratio. The value in uPixelAspRatio 
                  //allows specifying widest pixel 4:1 to the tallest pixel of 1:4 in increments of 1/64th
                  FLOAT pixelAspRatio = (uPixelAspRatio + 15.f) / 64.f;
                  //Calculate the image width and height in pixel based on the pixel aspect ratio. Only shrink the image.
                  if(pixelAspRatio > 1.f) 
                  { m_cxGifImagePixel = m_cxGifImage; m_cyGifImagePixel = static_cast<UINT>(m_cyGifImage / pixelAspRatio); }
                  else { m_cxGifImagePixel = static_cast<UINT>(m_cxGifImage * pixelAspRatio); m_cyGifImagePixel = m_cyGifImage; }
               }
               else { m_cxGifImagePixel = m_cxGifImage; m_cyGifImagePixel = m_cyGifImage; } //The value is 0, so its ratio is 1
            }
            PropVariantClear(&propValue);
         }
      }

      //Get looping information
      if(SUCCEEDED(hr))
      {
         // First check to see if the application block in the Application Extension
         // contains "NETSCAPE2.0" and "ANIMEXTS1.0", which indicates the gif animation has looping information associated with it.
         // If we fail to get the looping information, loop the animation infinitely.
         if (SUCCEEDED(pMetadataQueryReader->GetMetadataByName( L"/appext/application", &propValue)) &&
            propValue.vt == (VT_UI1 | VT_VECTOR) && propValue.caub.cElems == 11 &&  // Length of the application block
            (!memcmp(propValue.caub.pElems, "NETSCAPE2.0", propValue.caub.cElems) ||
            !memcmp(propValue.caub.pElems, "ANIMEXTS1.0", propValue.caub.cElems)))
         {
            PropVariantClear(&propValue);
            hr = pMetadataQueryReader->GetMetadataByName(L"/appext/data", &propValue);
            if (SUCCEEDED(hr))
            {
               //  byte 0: extsize (must be > 1)
               //  byte 1: loopType (1 == animated gif)
               //  byte 2: loop count (least significant byte)
               //  byte 3: loop count (most significant byte)
               //  byte 4: set to zero
               if(propValue.vt == (VT_UI1 | VT_VECTOR) && propValue.caub.cElems >= 4 && propValue.caub.pElems[0] > 0 && propValue.caub.pElems[1] == 1)
               {
                  m_uTotalLoopCount = MAKEWORD(propValue.caub.pElems[2], propValue.caub.pElems[3]);
                  // If the total loop count is not zero, we then have a loop count. If it is 0, then we repeat infinitely
                  if (m_uTotalLoopCount != 0) m_fHasLoop = TRUE;
               }
            }
         }
      }

      PropVariantClear(&propValue); SafeRelease(pMetadataQueryReader);
      return hr;
   }



   HRESULT CreateDeviceResources()
   {
      RECT rcClient; GetClientRect(hwnd, &rcClient);

      if(!m_pHwndRT)
      {
         // Create a D2D hwnd render target
         D2D1_RENDER_TARGET_PROPERTIES renderTargetProperties = D2D1::RenderTargetProperties();

         // Set the DPI to be the default system DPI to allow direct mapping
         // between image pixels and desktop pixels in different system DPI settings
         renderTargetProperties.dpiX = DEFAULT_DPI; renderTargetProperties.dpiY = DEFAULT_DPI;

         D2D1_HWND_RENDER_TARGET_PROPERTIES hwndRenderTragetproperties = D2D1::HwndRenderTargetProperties(hwnd,
            D2D1::SizeU(rcClient.right-rcClient.left, rcClient.bottom-rcClient.top));

         hr = m_pD2DFactory->CreateHwndRenderTarget( renderTargetProperties, hwndRenderTragetproperties, &m_pHwndRT);
      }
      else
      {
         // We already have a hwnd render target, resize it to the window size
         D2D1_SIZE_U size; size.width = rcClient.right-rcClient.left; size.height = rcClient.bottom-rcClient.top;
         hr = m_pHwndRT->Resize(size);
      }

      // Create a bitmap render target used to compose frames. Bitmap render targets cannot be resized, so we always recreate it.
      SafeRelease(m_pD2DBitmapRT);
      hr = m_pHwndRT->CreateCompatibleRenderTarget( D2D1::SizeF(static_cast<FLOAT>(m_cxGifImage),
         static_cast<FLOAT>(m_cyGifImage)), &m_pD2DBitmapRT);

      return hr;

   }


   //
   HRESULT OnRender()
   {
      ID2D1Bitmap *pFrameToRender = NULL;
      if(m_pHwndRT && m_pD2DBitmapRT) // Check to see if the render targets are initialized
      {
         // Only render when the window is not occluded
         if (!(m_pHwndRT->CheckWindowState() & D2D1_WINDOW_STATE_OCCLUDED))
         {
            D2D1_RECT_F drawRect; hr = CalculateDrawRectangle(drawRect);

            if (SUCCEEDED(hr))
            {
               // Get the bitmap to draw on the hwnd render target
               hr = m_pD2DBitmapRT->GetBitmap(&pFrameToRender);
            }

            if (SUCCEEDED(hr))
            {
               // Draw the bitmap onto the calculated rectangle
               m_pHwndRT->BeginDraw();
               m_pHwndRT->Clear(D2D1::ColorF(D2D1::ColorF::Black));
               m_pHwndRT->DrawBitmap(pFrameToRender, drawRect);
               hr = m_pHwndRT->EndDraw();
            }
         }
      }

      SafeRelease(pFrameToRender);
      return hr;

   }


   //
   HRESULT GetRawFrame(UINT uFrameIndex)
   {
      IWICFormatConverter *pConverter = NULL; IWICBitmapFrameDecode *pWicFrame = NULL;
      IWICMetadataQueryReader *pFrameMetadataQueryReader = NULL; PROPVARIANT propValue; PropVariantInit(&propValue);
      hr = m_pBitmapDecoder->GetFrame(uFrameIndex, &pWicFrame); //Retrieve the current frame
      if(SUCCEEDED(hr)) { hr = m_pIWICFactory->CreateFormatConverter(&pConverter); } //Format convert to 32bppPBGRA which D2D expects
      if(SUCCEEDED(hr))
      { hr = pConverter->Initialize( pWicFrame, GUID_WICPixelFormat32bppPBGRA, WICBitmapDitherTypeNone, NULL, 0.f, WICBitmapPaletteTypeCustom); }
      if(SUCCEEDED(hr))
      {
         //Create a D2DBitmap from IWICBitmapSource
         SafeRelease(m_pD2DBitmap); hr = m_pHwndRT->CreateBitmapFromWicBitmap( pConverter, NULL, &m_pD2DBitmap);
      }
      if(SUCCEEDED(hr)) { hr = pWicFrame->GetMetadataQueryReader(&pFrameMetadataQueryReader); }
      if(SUCCEEDED(hr)) //Get the Metadata for the current frame
      {
         hr = pFrameMetadataQueryReader->GetMetadataByName(L"/imgdesc/Left", &propValue);
         if (SUCCEEDED(hr))
         {
            hr = (propValue.vt == VT_UI2 ? S_OK : E_FAIL); 
            if (SUCCEEDED(hr)) m_framePosition.left = static_cast<FLOAT>(propValue.uiVal); PropVariantClear(&propValue);
         }
      }
      //
      if(SUCCEEDED(hr))
      {
         hr = pFrameMetadataQueryReader->GetMetadataByName(L"/imgdesc/Top", &propValue);
         if (SUCCEEDED(hr))
         {
            hr = (propValue.vt == VT_UI2 ? S_OK : E_FAIL); 
            if (SUCCEEDED(hr)) m_framePosition.top = static_cast<FLOAT>(propValue.uiVal); PropVariantClear(&propValue);
         }
      }
      //
      if(SUCCEEDED(hr))
      {
         hr = pFrameMetadataQueryReader->GetMetadataByName(L"/imgdesc/Width", &propValue);
         if (SUCCEEDED(hr))
         {
            hr = (propValue.vt == VT_UI2 ? S_OK : E_FAIL); 
            if(SUCCEEDED(hr))
            {  m_framePosition.right = static_cast<FLOAT>(propValue.uiVal) + m_framePosition.left; }
            PropVariantClear(&propValue);
         }
      }
      if (SUCCEEDED(hr))
      {
         hr = pFrameMetadataQueryReader->GetMetadataByName(L"/imgdesc/Height", &propValue);
         if (SUCCEEDED(hr))
         {
            hr = (propValue.vt == VT_UI2 ? S_OK : E_FAIL);
            if (SUCCEEDED(hr)) { m_framePosition.bottom = static_cast<FLOAT>(propValue.uiVal) + m_framePosition.top; }
            PropVariantClear(&propValue);
         }
      }

      if (SUCCEEDED(hr))
      {
         // Get delay from the optional Graphic Control Extension
         if (SUCCEEDED(pFrameMetadataQueryReader->GetMetadataByName( L"/grctlext/Delay", &propValue)))
         {
            hr = (propValue.vt == VT_UI2 ? S_OK : E_FAIL); 
            if (SUCCEEDED(hr))
            {
               //Convert the delay retrieved in 10 ms units to a delay in 1 ms units
               hr = UIntMult(propValue.uiVal, 10, &m_uFrameDelay);
            }
            PropVariantClear(&propValue);
         }
         else m_uFrameDelay = 0; //Failed to get delay from graphic control extension. Possibly a single frame image
         if (SUCCEEDED(hr))
         {
            // Insert an artificial delay to ensure rendering for gif with very small
            // or 0 delay.  This delay number is picked to match with most browsers' gif display speed.
            // This will defeat the purpose of using zero delay intermediate frames in 
            // order to preserve compatibility. If this is removed, the zero delay intermediate frames will not be visible.
            if (m_uFrameDelay < 90) m_uFrameDelay = 90;
         }
      }
      if (SUCCEEDED(hr))
      {
         if (SUCCEEDED(pFrameMetadataQueryReader->GetMetadataByName(L"/grctlext/Disposal", &propValue)))
         { hr = (propValue.vt == VT_UI1) ? S_OK : E_FAIL; if (SUCCEEDED(hr)) m_uFrameDisposal = propValue.bVal; }
         else m_uFrameDisposal = DM_UNDEFINED; //Failed to get the disposal method, use default. Possibly a non-animated gif.
      }

      PropVariantClear(&propValue); SafeRelease(pConverter); SafeRelease(pWicFrame); SafeRelease(pFrameMetadataQueryReader);
      return hr;
   }


   //
   HRESULT GetBackgroundColor(IWICMetadataQueryReader *pMetadataQueryReader)
   {
      DWORD dwBGColor; BYTE backgroundIndex = 0; WICColor rgColors[256]; UINT cColorsCopied = 0;
      PROPVARIANT propVariant; PropVariantInit(&propVariant); IWICPalette *pWicPalette = NULL;

      //If we have a global palette, get the palette and background color
      hr = pMetadataQueryReader->GetMetadataByName(L"/logscrdesc/GlobalColorTableFlag", &propVariant);
      hr = (propVariant.vt != VT_BOOL || !propVariant.boolVal) ? E_FAIL : S_OK; PropVariantClear(&propVariant);

      if(SUCCEEDED(hr))
      {
         hr = pMetadataQueryReader->GetMetadataByName(L"/logscrdesc/BackgroundColorIndex", &propVariant);
         if (SUCCEEDED(hr))
         {
            hr = (propVariant.vt != VT_UI1) ? E_FAIL : S_OK; if(SUCCEEDED(hr)) backgroundIndex = propVariant.bVal;
            PropVariantClear(&propVariant);
         }
      }

      //Get the color from the palette
      if (SUCCEEDED(hr)) hr = m_pIWICFactory->CreatePalette(&pWicPalette);
      if (SUCCEEDED(hr)) hr = m_pBitmapDecoder->CopyPalette(pWicPalette); //Get the global palette
      if (SUCCEEDED(hr)) hr = pWicPalette->GetColors( ARRAYSIZE(rgColors), rgColors, &cColorsCopied);
      if (SUCCEEDED(hr)) { hr = (backgroundIndex >= cColorsCopied) ? E_FAIL : S_OK; }
      if (SUCCEEDED(hr))
      {
         dwBGColor = rgColors[backgroundIndex];
         FLOAT alpha = (dwBGColor >> 24) / 255.f; m_backgroundColor = D2D1::ColorF(dwBGColor, alpha);
      }

      SafeRelease(pWicPalette);
      return hr;
   }


   //
   HRESULT ClearCurrentFrameArea()
   {
      m_pD2DBitmapRT->BeginDraw();
      m_pD2DBitmapRT->PushAxisAlignedClip(&m_framePosition, D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
      m_pD2DBitmapRT->Clear(m_backgroundColor); m_pD2DBitmapRT->PopAxisAlignedClip(); //Remove the clipping
      return m_pD2DBitmapRT->EndDraw();
   }


   //
   BOOL IsLastFrame() { return (m_uNextFrameIndex == 0); }
   BOOL EndOfAnimation() { return m_fHasLoop && IsLastFrame() && (m_uLoopNumber == m_uTotalLoopCount + 1); }

   //
   HRESULT CalculateDrawRectangle(D2D1_RECT_F &drawRect)
   {
      RECT rcClient; GetClientRect(hwnd, &rcClient);
      drawRect.left = static_cast<FLOAT>(rcClient.left); drawRect.top = static_cast<FLOAT>(rcClient.top);
      drawRect.right = drawRect.left + m_cxGifImagePixel; drawRect.bottom = drawRect.top + m_cyGifImagePixel;
      return hr;
   }


   //
   HRESULT DisposeCurrentFrame()
   {
      switch(m_uFrameDisposal)
      {
      case DM_UNDEFINED: case DM_NONE: break; //We simply draw on the previous frames. Do nothing here.
      case DM_BACKGROUND: //Dispose background. Clear the area covered by the current raw frame with background color
         hr = ClearCurrentFrameArea(); break;
      case DM_PREVIOUS: hr = RestoreSavedFrame(); break; //Dispose previous. We restore the previous composed frame first
      default: hr = E_FAIL;
      }
      return hr;
   }


   //
   HRESULT OverlayNextFrame()
   {
      hr = GetRawFrame(m_uNextFrameIndex); //Get Frame information
      //For disposal 3 method, we would want to save a copy of the current composed frame
      if(m_uFrameDisposal == DM_PREVIOUS) hr = SaveComposedFrame();
      if(SUCCEEDED(hr))
      {
         m_pD2DBitmapRT->BeginDraw(); //Start producing the next bitmap
         if(!m_uNextFrameIndex) //If starting a new animation loop
         {
            m_pD2DBitmapRT->Clear(m_backgroundColor); m_uLoopNumber++; //Draw background and increase loop count
         }
         m_pD2DBitmapRT->DrawBitmap(m_pD2DBitmap, m_framePosition); hr = m_pD2DBitmapRT->EndDraw(); //Produce the next frame
      }

      //To improve performance and avoid decoding/composing this frame in the 
      //following animation loops, the composed frame can be cached here in system or video memory.
      if (SUCCEEDED(hr)) { m_uNextFrameIndex = (++m_uNextFrameIndex) % m_cFrames; } //Increase the frame index by 1

      return hr;
   }

   //
   HRESULT SaveComposedFrame()
   {
      ID2D1Bitmap *pFrameToBeSaved = NULL; hr = m_pD2DBitmapRT->GetBitmap(&pFrameToBeSaved);
      if(SUCCEEDED(hr))
      {
         if(!m_pSavedFrame) //Create the temporary bitmap if it hasn't been created yet 
         {
            D2D1_SIZE_U bitmapSize = pFrameToBeSaved->GetPixelSize(); D2D1_BITMAP_PROPERTIES bitmapProp;
            pFrameToBeSaved->GetDpi(&bitmapProp.dpiX, &bitmapProp.dpiY);
            bitmapProp.pixelFormat = pFrameToBeSaved->GetPixelFormat();
            hr = m_pD2DBitmapRT->CreateBitmap(  bitmapSize,  bitmapProp, &m_pSavedFrame);
         }
      }
      hr = m_pSavedFrame->CopyFromBitmap(NULL, pFrameToBeSaved, NULL); SafeRelease(pFrameToBeSaved);
      return hr;
   }



   //
   HRESULT RestoreSavedFrame()
   {
      ID2D1Bitmap *pFrameToCopyTo = NULL; hr = m_pSavedFrame ? S_OK : E_FAIL;
      hr = m_pD2DBitmapRT->GetBitmap(&pFrameToCopyTo);
      hr = pFrameToCopyTo->CopyFromBitmap(NULL, m_pSavedFrame, NULL); SafeRelease(pFrameToCopyTo);
      return hr;
   }


   //
   HRESULT ComposeNextFrame()
   {
      if(m_pHwndRT && m_pD2DBitmapRT) //Check to see if the render targets are initialized
      {
         KillTimer(hwnd, 1); DisposeCurrentFrame(); hr = OverlayNextFrame(); //Compose one frame
         //Keep composing frames until we see a frame with delay greater than
         //0 (0 delay frames are the invisible intermediate frames), or until we have reached the very last frame.
         while(SUCCEEDED(hr) && !m_uFrameDelay && IsLastFrame()) { hr = DisposeCurrentFrame(); hr = OverlayNextFrame(); }
         //If we have more frames to play, set the timer according to the delay.
         //Set the timer regardless of whether we succeeded in composing a frame to try our best to continue displaying the animation.
         if(!EndOfAnimation() && m_cFrames) SetTimer(hwnd, 1, m_uFrameDelay, NULL);
      }
      return hr;
   }


   //
   HRESULT RecoverDeviceResources()
   {
      SafeRelease(m_pHwndRT); SafeRelease(m_pD2DBitmapRT); SafeRelease(m_pSavedFrame);
      m_uNextFrameIndex = 0; m_uLoopNumber = 0; m_uFrameDisposal = DM_NONE; //No previous frames. Use disposal none.
      hr = CreateDeviceResources();
      if(m_cFrames > 0) { hr = ComposeNextFrame(); InvalidateRect(hwnd, NULL, FALSE); } //Load the first frame
      return hr;
   }


   //
   LONG_PTR OnTimer(WPARAM, LPARAM) { ComposeNextFrame(); Invalidate(); return 0L; }

   //
   int OnPaint(HDC hdc) { OnRender(); ValidateRect(hwnd, NULL); return 0; }

   //
   LONG_PTR OnDisplayChange(WPARAM, LPARAM) { Invalidate(); return 0L; }

};

